<html>
<head>
  <script type="text/javascript" src="webrtc_test_utilities.js"></script>
  <script type="text/javascript">
  $ = function(id) {
    return document.getElementById(id);
  };

  // The modal permission dialog requires a user gesture to trigger.
  // Hook up a click event listener to run a specified method (which
  // may be changed by the test).
  var functionToRun;
  function runFunctionOnClick() {
      eval(functionToRun);
  }

  window.addEventListener('load', () => {
      window.addEventListener('click', runFunctionOnClick);
  });

  function getSources() {
    navigator.mediaDevices.enumerateDevices().then(function(devices) {
      document.title = 'Media devices available';
      var results = [];
      for (var device, i = 0; device = devices[i]; ++i) {
        if (device.kind != "audioinput" && device.kind != "videoinput")
          continue;
        results.push({
          'id': device.deviceId,
          'kind': device.kind == "audioinput" ? "audio" : "video",
          'label': device.label,
          'facing': ""
        });
      }
      sendValueToTest(JSON.stringify(results));
    });
  }

  // Creates a MediaStream and renders it locally. When the video is detected to
  // be rolling, the stream should be stopped.
  function getUserMediaAndStop(constraints) {
    console.log('Calling getUserMediaAndStop. constraints : ' +
        JSON.stringify(constraints));
    navigator.mediaDevices.getUserMedia(constraints)
        .then(stream => {
          window.document.title = 'Granted';
          detectVideoInLocalView1(stream).then(() => {
            stream.getVideoTracks()[0].stop();
            detectVideoStopped('local-view-1')
                .then(reportTestSuccess);
          });
        })
        .catch(() => {
          window.document.title = 'Denied';
          failTest();
        });
  }

  // Requests getusermedia and expects it to succeed.
  function getUserMediaAndExpectSuccess(constraints) {
    console.log('Calling getUserMediaAndExpectSuccess.');
    navigator.mediaDevices.getUserMedia(constraints)
        .then(reportTestSuccess)
        .catch(failTest);
  }

  // Requests getusermedia and expects it to fail. The error name is returned
  // to the test.
  function getUserMediaAndExpectFailure(constraints) {
    console.log('Calling getUserMediaAndExpectFailure.');
    navigator.mediaDevices.getUserMedia(constraints)
        .then(stream => {
          failTest('Unexpectedly succeeded getUserMedia.');
        })
        .catch(error => { sendValueToTest(error.name); });
  }

  function renderClonedMediastreamAndStop(constraints, waitTimeInSeconds) {
    console.log('Calling renderClonedMediastreamAndStop.');
    navigator.mediaDevices.getUserMedia(constraints)
        .then(stream => {
          var duplicate = stream.clone();
          assertEquals(stream.getVideoTracks().length, 1);
          assertEquals(duplicate.getVideoTracks().length, 1);
          assertNotEquals(stream.getVideoTracks()[0].id,
                          duplicate.getVideoTracks()[0].id);
          detectVideoInLocalView1(stream)
              .then(reportTestSuccess);
        })
        .catch(failTest);
  }

  function renderDuplicatedMediastreamAndStop(constraints, waitTimeInSeconds) {
    console.log('Calling renderDuplicateMediastreamAndStop.');
    navigator.mediaDevices.getUserMedia(constraints)
        .then(stream => {
          var duplicate = new MediaStream(stream);
          assertEquals(stream.getVideoTracks().length, 1);
          assertEquals(duplicate.getVideoTracks().length, 1);
          assertEquals(stream.getVideoTracks()[0].id,
                       duplicate.getVideoTracks()[0].id);
          detectVideoInLocalView1(duplicate)
              .then(reportTestSuccess);
        })
        .catch(failTest);
  }

  function renderSameTrackMediastreamAndStop(constraints, waitTimeInSeconds) {
    console.log('Calling renderSameTrackMediastreamAndStop.');
    navigator.mediaDevices.getUserMedia(constraints)
        .then(stream => {
          var duplicate = new MediaStream();
          duplicate.addTrack(stream.getVideoTracks()[0]);
          assertEquals(duplicate.getVideoTracks().length, 1);
          assertEquals(duplicate.getVideoTracks().length, 1);
          assertEquals(stream.getVideoTracks()[0].id,
                       duplicate.getVideoTracks()[0].id);
          detectVideoInLocalView1(duplicate)
              .then(reportTestSuccess);
        })
        .catch(failTest);
  }

  function renderClonedTrackMediastreamAndStop(constraints, waitTimeInSeconds) {
    console.log('Calling renderClonedTrackMediastreamAndStop.');
    navigator.mediaDevices.getUserMedia(constraints)
        .then(stream => {
          var duplicate = new MediaStream();
          duplicate.addTrack(stream.getVideoTracks()[0].clone());
          assertEquals(duplicate.getVideoTracks().length, 1);
          assertEquals(duplicate.getVideoTracks().length, 1);
          assertNotEquals(stream.getVideoTracks()[0].id,
                          duplicate.getVideoTracks()[0].id)
          detectVideoInLocalView1(duplicate)
              .then(reportTestSuccess);
        })
        .catch(failTest);
  }

  function getUserMediaAndRenderInSeveralVideoTags() {
    // This test makes sure multiple video renderers can be created for the same
    // local video track, and makes sure a renderer can still render if other
    // renderers are paused. See http://crbug/352619.
    function createDetectableRenderer(stream, id) {
      var video = document.createElement('video');
      document.body.appendChild(video);
      video.id = id;
      video.style.display = 'none'
      video.srcObject = stream;
      video.autoplay = true;
      video.play();

      // The detector needs a canvas.
      var canvas = document.createElement('canvas');
      canvas.id = video.id + "-canvas";
      canvas.style.display = 'none'
      document.body.appendChild(canvas);
    };

    navigator.mediaDevices.getUserMedia({video: true}).then(stream => {
      // Create 3 video renderers and pause them once video is playing.
      var createdAndPaused = [];
      for (var i = 0; i < 3; ++i) {
        var id = "video" + i;
        createDetectableRenderer(stream, id);

        // Detect video id |id| and trigger the function returned by
        // |video_detected_function| when video is playing.
        var promise = detectVideoPlaying(id)
            .then(() => {
              console.log("pause " + id);
              $(id).pause();
            });
        createdAndPaused.push(promise);
      }

      Promise.all(createdAndPaused)
        .then(() => {
          // Create one last renderer and make sure it can play video.
          var id = "lastVideoTag";
          createDetectableRenderer(stream, id);
          return detectVideoPlaying(id);
        })
        .then(reportTestSuccess);
    })
    .catch(failTest);
  }

  // Gets a video stream up, analyses it and returns the aspect ratio to the
  // test through the automation controller.
  function getUserMediaAndAnalyseAndStop(constraints) {
    console.log('Calling getUserMediaAndAnalyseAndStop.');
    navigator.mediaDevices.getUserMedia(constraints)
        .then(displayDetectAndAnalyzeVideo)
        .catch(failTest);
  }

  // This test that a MediaStream can be cloned and that the clone can
  // be rendered.
  function getUserMediaAndClone() {
    console.log('Calling getUserMediaAndClone.');
    navigator.mediaDevices.getUserMedia({video: true, audio: true})
        .then(createAndRenderClone)
        .catch(failTest);
  }

  // Creates two MediaStream and renders them locally. When the video of both
  // streams are detected to be rolling, we stop the local video tracks one at
  // the time. In particular, we verify that stopping one track does not stop
  // the other.
  function twoGetUserMediaAndStop(constraints) {
    var stream1 = null;
    var stream2 = null;
    navigator.mediaDevices.getUserMedia(constraints)
        .then(stream => {
          stream1 = stream;
          $('local-view-1').srcObject = stream;
          requestSecondGetUserMedia();
        })
        .catch(failTest);
    var requestSecondGetUserMedia = function() {
      navigator.mediaDevices.getUserMedia(constraints)
          .then(stream => {
            stream2 = stream;
            $('local-view-2').srcObject = stream;
            stopBothVideoTracksAndVerify();
          })
          .catch(failTest);
    };

    var stopBothVideoTracksAndVerify = function() {
      // Stop track 2, ensure track 2 stops but not track 1, then stop track 1.
      stream2.getVideoTracks()[0].stop();
      detectVideoStopped('local-view-2').then(() => {
        detectVideoInLocalView1(stream1).then(() => {
          stream1.getVideoTracks()[0].stop();
          detectVideoStopped('local-view-1')
              .then(reportTestSuccess);
        });
      });
    };
  }

  function twoGetUserMedia(constraints1, constraints2) {
    var result="";
    navigator.mediaDevices.getUserMedia(constraints1)
        .then(stream => {
          displayDetectAndAnalyzeVideoInElement(
              stream,
              function(aspectRatio) {
                result = aspectRatio;
                requestSecondGetUserMedia();
              },
              'local-view-1');
        })
        .catch(failTest);
    var requestSecondGetUserMedia = function() {
      navigator.mediaDevices.getUserMedia(constraints2)
          .then(stream => {
            displayDetectAndAnalyzeVideoInElement(
                stream,
                function(aspectRatio) {
                  result = result + '-' + aspectRatio;
                  sendValueToTest(result);
                },
                'local-view-2');
          })
          .catch(failTest);
    }
  }

  function getUserMediaInIframeAndCloseInSuccessCb(constraints) {
    var iframe = document.createElement('iframe');
    iframe.onload = onIframeLoaded;
    document.body.appendChild(iframe);
    iframe.src = window.location;

    function onIframeLoaded() {
      var iframe  = window.document.querySelector('iframe');
      iframe.contentWindow.navigator.mediaDevices.getUserMedia(constraints)
          .then(stream => {
            // Remove the iframe from the parent within the callback scope.
            window.parent.document.querySelector('iframe').remove();
            // This function enqueues reporting test success, rather than doing
            // it directly. We do this so we catch crashes that occur in the
            // current execution context, but after reportTestSuccess is
            // invoked.
            setTimeout(function () {
              window.parent.reportTestSuccess();
            }, 0);
          })
          .catch(window.parent.failTest);
    }
  }

  function getUserMediaInIframeAndCloseInFailureCb(constraints) {
    var iframe = document.createElement('iframe');
    iframe.onload = onIframeLoaded;
    document.body.appendChild(iframe);
    iframe.src = window.location;

    function onIframeLoaded() {
      var iframe  = window.document.querySelector('iframe');
      iframe.contentWindow.navigator.mediaDevices.getUserMedia(constraints)
          .then(stream => {
            window.parent.failTest('GetUserMedia call succeeded unexpectedly.');
          })
          .catch(error => {
            // Remove the iframe from the parent within the callback scope.
            window.parent.document.querySelector('iframe').remove();
            // This function enqueues reporting test success, rather than doing
            // it directly. We do this so we catch crashes that occur in the
            // current execution context, but after reportTestSuccess is
            // invoked.
            setTimeout(function () {
              window.parent.reportTestSuccess();
            }, 0);
          });
    }
  }

  function detectVideoInLocalView1(stream) {
    $('local-view-1').srcObject = stream;
    return detectVideoPlaying('local-view-1');
  }

  function displayDetectAndAnalyzeVideo(stream) {
    displayDetectAndAnalyzeVideoInElement(stream,
        function(aspectRatio) {
          sendValueToTest(aspectRatio);
        },
        'local-view-1');
  }

  function displayDetectAndAnalyzeVideoInElement(
      stream, callback, videoElementName) {
    $(videoElementName).srcObject = stream;
    var videoElement = $(videoElementName);
    videoElement.onloadedmetadata = function () {
      detectAspectRatio(callback, videoElement);
    };
  }

  function createAndRenderClone(stream) {
    // TODO(perkj):  --use-fake-device-for-media-stream do not currently
    // work with audio devices and not all bots has a microphone.
    newStream = new MediaStream();
    newStream.addTrack(stream.getVideoTracks()[0]);
    assertEquals(newStream.getVideoTracks().length, 1);
    if (stream.getAudioTracks().length > 0) {
      newStream.addTrack(stream.getAudioTracks()[0]);
      assertEquals(newStream.getAudioTracks().length, 1);
      newStream.removeTrack(newStream.getAudioTracks()[0]);
      assertEquals(newStream.getAudioTracks().length, 0);
    }

    detectVideoInLocalView1(newStream)
        .then(reportTestSuccess);
  }

  // Calls stop on |stream|'s video track after a delay and reports success.
  function waitAndStopVideoTrack(stream, waitTimeInSeconds) {
    setTimeout(function() {
      stream.getVideoTracks()[0].stop();
      reportTestSuccess();
    }, waitTimeInSeconds * 1000);
  }

  // This function tries to calculate the aspect ratio shown by the fake capture
  // device in the video tag. For this, we count the amount of light green
  // pixels along |aperture| pixels on the positive X and Y axis starting from
  // the center of the image. In this very center there should be a time-varying
  // pacman; the algorithm counts for a couple of iterations and keeps the
  // maximum amount of light green pixels on both directions. From this data
  // the aspect ratio is calculated and the test fails if the number of green
  // pixels are not the same along the X and Y axis.
  // The result of the analysis is sent back to the test as a string on the
  // format "w=xxx:h=yyy".
  function detectAspectRatio(callback, videoElement) {
    var canvas = $(videoElement.id + '-canvas');

    var maxLightGreenPixelsX = 0;
    var maxLightGreenPixelsY = 0;

    var attempt = 0;
    var maxAttempts = 10;

    var detectorFunction = function() {
      var width = videoElement.videoWidth;
      var height = videoElement.videoHeight;
      if (width == 0 || height == 0)
        failTest("VideoElement width and height set to 0.");

      canvas.width = width;
      canvas.height = height;
      var aperture = Math.min(width, height) / 2;
      var context = canvas.getContext('2d');
      context.drawImage(videoElement, 0, 0, width, height);

      // We are interested in a window starting from the center of the image
      // where we expect the circle from the fake video capture to be rolling.
      var pixels = context.getImageData(width / 2, height / 2,
                                        aperture, aperture);

      var lightGreenPixelsX = 0;
      var lightGreenPixelsY = 0;

      // Walk horizontally counting light green pixels.
      for (var x = 0; x < aperture; ++x) {
        if (!isAlmostBackgroundGreen(pixels.data[4 * x + 1]))
          lightGreenPixelsX++;
      }
      // Walk vertically counting light green pixels.
      for (var y = 0; y < aperture; ++y) {
        if (!isAlmostBackgroundGreen(pixels.data[4 * y * aperture + 1]))
          lightGreenPixelsY++;
      }
      if (lightGreenPixelsX > maxLightGreenPixelsX)
        maxLightGreenPixelsX = lightGreenPixelsX;
      if (lightGreenPixelsY > maxLightGreenPixelsY)
        maxLightGreenPixelsY = lightGreenPixelsY;

      // Allow maxLightGreenPixelsY = maxLightGreenPixelsX +-1 due to
      // possible subpixel rendering on Mac and Android.
      if (maxLightGreenPixelsY > maxLightGreenPixelsX + 1 ||
          maxLightGreenPixelsY < maxLightGreenPixelsX -1 ||
          maxLightGreenPixelsY == 0 ||
          maxLightGreenPixelsX == width / 2 ||
          maxLightGreenPixelsY == height / 2) {
        if (++attempt > maxAttempts) {
          clearInterval(detectorInterval);
          failTest("Aspect ratio corrupted. X " + maxLightGreenPixelsX  +
                   " Y " + maxLightGreenPixelsY + " width " + width +
                   " height " + height);
        }
        else {
          // We have a bad aspect ratio now; give a chance to shape up.
          return;
        }
      }

      clearInterval(detectorInterval);
      var result = "w=" + width + ":h=" + height;
      callback(result);
    }
    var detectorInterval = setInterval(detectorFunction, 50);
  }

  function getAudioSettingsDefault() {
    navigator.mediaDevices.getUserMedia({audio:true})
      .then(stream => {
        assertEquals(stream.getAudioTracks().length, 1);
        var settings = stream.getAudioTracks()[0].getSettings();
        assertEquals(settings.deviceId, 'default');
        assertTrue(settings.echoCancellation);
        stream.getAudioTracks()[0].stop();
        reportTestSuccess();
      })
      .catch(_ => {
        failTest("getUserMedia failed")
      });
  }

  function getAudioSettingsNoEchoCancellation() {
    navigator.mediaDevices.getUserMedia({audio:{echoCancellation: false}})
      .then(stream => {
        assertEquals(stream.getAudioTracks().length, 1);
        var settings = stream.getAudioTracks()[0].getSettings();
        assertEquals(settings.deviceId, 'default');
        assertEquals(settings.echoCancellation, false);
        stream.getAudioTracks()[0].stop();
        reportTestSuccess();
      })
      .catch(_ => {
        failTest("getUserMedia failed")
      });
  }

  function getAudioSettingsDeviceId() {
    navigator.mediaDevices.enumerateDevices()
      .then(devices => {
        var last_device_id;
        for (var device, i = 0; device = devices[i]; ++i) {
          if (device.kind != "audioinput")
            continue;
          last_device_id = device.deviceId;
        }
        navigator.mediaDevices.getUserMedia(
          {audio:{deviceId: {exact: last_device_id}}})
          .then(stream => {
            assertEquals(stream.getAudioTracks().length, 1);
            var settings = stream.getAudioTracks()[0].getSettings();
            assertEquals(settings.deviceId, last_device_id);
            assertNotEquals(settings.deviceId, 'default');
            assertTrue(settings.echoCancellation);
            stream.getAudioTracks()[0].stop();
            reportTestSuccess();
          })
      })
      .catch(failTest);
  }

  function srcObjectAddVideoTrack() {
    var video = document.createElement('video');
    video.autoplay = true;
    assertEquals(video.srcObject, null);
    navigator.mediaDevices.getUserMedia({audio: true, video: true})
      .then(stream => {
        video.onplaying = function() {
          video.onplaying = null;
          video.onloadstart = function() {
            failTest("loadstart should not be called");
          }

          video.onresize = function() {
            assertNotEquals(video.srcObject, null);
            assertTrue(video.videoHeight > 0);
            assertTrue(video.videoWidth > 0);
            reportTestSuccess();
          }

          assertNotEquals(video.srcObject, null);
          assertEquals(video.videoWidth, 0);
          assertEquals(video.videoHeight, 0);
          video.srcObject.addTrack(stream.getVideoTracks()[0]);
        }
        video.srcObject = new MediaStream(stream.getAudioTracks());
      })
    .catch(failTest);
  }

  async function srcObjectReplaceInactiveTracks() {
    let video = document.createElement('video');
    video.autoplay = true;
    assertEquals(video.srcObject, null);
    try {
      let stream = await navigator.mediaDevices.getUserMedia(
          {audio: true, video: true});
      video.onplaying = () => {
        video.onplaying = null;
        video.onloadstart = () => {
          failTest("loadstart should not be fired");
        }

        assertNotEquals(video.srcObject, null);
        assertTrue(video.videoWidth > 0);
        assertTrue(video.videoHeight > 0);

        // Stop tracks.
        stream.getTracks().forEach(track => track.stop());
        let onprogress_calls = 0;
        video.onprogress = () => {
          assertEquals(video.networkState, HTMLMediaElement.NETWORK_IDLE);
          setTimeout(reportTestSuccess, 100);
          if (++onprogress_calls > 1)
            failTest('progress should not be fired more than once');
        }
      }

      video.srcObject = stream;
    } catch (err) {
      failTest('getUserMedia() failed');
    }
  }

  function srcObjectRemoveVideoTrack() {
    var video = document.createElement('video');
    video.autoplay = true;
    assertEquals(video.srcObject, null);
    navigator.mediaDevices.getUserMedia({audio: true, video: true})
      .then(stream => {
        video.onplaying = function() {
          video.onplaying = null;
          video.onloadstart = function() {
            failTest("loadstart should not be called");
          }

          video.onresize = function() {
            assertNotEquals(video.srcObject, null);
            assertEquals(0, video.videoHeight);
            assertEquals(0, video.videoWidth);
            reportTestSuccess();
          }

          assertNotEquals(video.srcObject, null);
          assertTrue(video.videoWidth > 0);
          assertTrue(video.videoHeight > 0);
          stream.removeTrack(stream.getVideoTracks()[0]);
        }
        video.srcObject = stream;
      })
    .catch(failTest);
  }

  function srcObjectRemoveFirstOfTwoVideoTracks() {
    var canvas = document.createElement('canvas');
    var canvas_stream = canvas.captureStream();
    var canvas_width = canvas_stream.getVideoTracks()[0].getSettings().width;
    var canvas_height = canvas_stream.getVideoTracks()[0].getSettings().height;
    assertTrue(canvas_width > 1);
    assertTrue(canvas_height > 1);

    // Paint something on the canvas, so that it produces frames.
    var ctx = canvas.getContext("2d");
    ctx.moveTo(0,0);
    ctx.lineTo(200,100);
    ctx.stroke();

    var video = document.createElement('video');
    video.autoplay = true;
    assertEquals(video.srcObject, null);
    var gum_width = canvas_width + 1;
    var gum_height = canvas_height + 1;
    navigator.mediaDevices.getUserMedia({
        video: {
          width: {exact: gum_width},
          height: {exact: gum_height}
        }
      }).then(gum_stream => {
        var gum_settings = gum_stream.getVideoTracks()[0].getSettings();
        assertEquals(gum_width, gum_settings.width)
        assertEquals(gum_height, gum_settings.height)
        var big_stream = new MediaStream();
        big_stream.addTrack(canvas_stream.getVideoTracks()[0]);
        big_stream.addTrack(gum_stream.getVideoTracks()[0]);
        video.onloadedmetadata = function() {
          assertEquals(canvas_width, video.videoWidth);
          assertEquals(canvas_height, video.videoHeight);
          assertNotEquals(video.videoWidth, gum_width)
          assertNotEquals(video.videoHeight, gum_height)
          video.onprogress = function() {
            assertEquals(gum_width, video.videoWidth);
            assertEquals(gum_height, video.videoHeight);
            assertNotEquals(video.videoWidth, canvas_width)
            assertNotEquals(video.videoHeight, canvas_height)
            reportTestSuccess();
          }
          big_stream.removeTrack(big_stream.getVideoTracks()[0]);
        }
        video.srcObject = big_stream;
    })
    .catch(failTest);
  }

  function srcObjectReassignSameObject() {
    var video = document.createElement('video');
    video.autoplay = true;
    assertEquals(video.srcObject, null);
    navigator.mediaDevices.getUserMedia({audio: true, video: true})
      .then(stream => {
        video.onplaying = function() {
          video.onplaying = null;
          video.onloadstart = function() {
            reportTestSuccess();
          }
          assertNotEquals(video.srcObject, null);
          assertTrue(video.videoWidth > 0);
          assertTrue(video.videoHeight > 0);
          // Reassigning the same object should trigger the load algorithm.
          video.srcObject = video.srcObject;
        }
        video.srcObject = stream;
      })
    .catch(failTest);
  }

  function applyConstraintsVideo() {
    navigator.mediaDevices.getUserMedia({video:true})
      .then(stream => {
        assertEquals(stream.getVideoTracks().length, 1);
        var track = stream.getVideoTracks()[0];
        var settings = track.getSettings();
        var default_device_id = settings.deviceId;
        var default_width = settings.width;
        var default_height = settings.height;
        return track.applyConstraints({
          width: default_width-1,
          height: default_height-1
        }).then(()=> {
          settings = track.getSettings();
          assertEquals(settings.deviceId, default_device_id);
          assertEquals(settings.width, default_width - 1);
          assertEquals(settings.height, default_height - 1);
          track.stop();
          reportTestSuccess();
        });
      })
      .catch(failTest);
  }

  function applyConstraintsVideoTwoStreams() {
    navigator.mediaDevices.getUserMedia({
        video:{
            width:{exact:640},
            height: {exact:480}
        }
    }).then(stream => {
        assertEquals(stream.getVideoTracks().length, 1);
        let track1 = stream.getVideoTracks()[0];
        let deviceId1 = track1.getSettings().deviceId;
        track1.onmute = () => failTest("Unexpected mute of track1");
        track1.onunmute = () => failTest("Unexpected unmute of track1");
        return navigator.mediaDevices.getUserMedia({
            video:{
                deviceId: {exact: deviceId1},
                width:{exact:640},
                height: {exact:480}
            }
        }).then(stream2 => {
            let track2 = stream2.getVideoTracks()[0];
            track2.onmute = () => failTest("Unexpected mute of track2");
            track2.onunmute = () => failTest("Unexpected unmute of track2");
            return track1.applyConstraints({
              width: {exact: 639},
              height: {exact: 479}
            }).then(()=> {
              assertEquals(track1.readyState, "live");
              let settings1 = track1.getSettings();
              assertEquals(639, settings1.width);
              assertEquals(479, settings1.height);
              assertEquals(track2.readyState, "live");
              let settings2 = track2.getSettings();
              assertEquals(640, settings2.width);
              assertEquals(480, settings2.height);
              assertEquals(deviceId1, settings1.deviceId);
              assertEquals(deviceId1, settings2.deviceId);
              track1.stop();
              track2.stop();
              reportTestSuccess();
            });
          })
      })
      .catch(failTest);
  }

  function applyConstraintsVideoOverconstrained() {
    navigator.mediaDevices.getUserMedia({video:true})
      .then(stream => {
        assertEquals(stream.getVideoTracks().length, 1);
        var track = stream.getVideoTracks()[0];
        var settings = track.getSettings();
        var default_device_id = settings.deviceId;
        var default_width = settings.width;
        var default_height = settings.height;
        track.applyConstraints({
          width: {exact: 1000000},
        }).then(()=> {
          failTest("applyConstraints succeeded with impossible constraints");
        }).catch(e => {
          assertEquals(e.name, "OverconstrainedError");
          assertEquals(e.constraint, "width");
          assertTrue(!!e.message);
          settings = track.getSettings();
          assertEquals(settings.deviceId, default_device_id);
          assertEquals(settings.width, default_width);
          assertEquals(settings.height, default_height);
          track.stop();
          reportTestSuccess();
        });
      })
      .catch(failTest);
  }

  function applyConstraintsNonDevice() {
    var canvas = document.createElement('canvas');
    var canvas_stream = canvas.captureStream();
    var canvas_track = canvas_stream.getVideoTracks()[0];
    var canvas_width = canvas_track.getSettings().width;
    var canvas_height = canvas_track.getSettings().height;
    assertTrue(canvas_width > 1);
    assertTrue(canvas_height > 1);

    // Paint something on the canvas, so that it produces frames.
    var ctx = canvas.getContext("2d");
    ctx.moveTo(0,0);
    ctx.lineTo(200,100);
    ctx.stroke();

    var video = document.createElement('video');
    video.autoplay = true;
    assertEquals(video.srcObject, null);
    video.onloadedmetadata = () => {
      assertEquals(video.videoWidth, canvas_width);
      assertEquals(video.videoHeight, canvas_height);
      var new_width = canvas_width - 1;
      var new_height = canvas_height - 1;
      video.ontimeupdate = () => {
        assertEquals(video.videoWidth, new_width);
        assertEquals(video.videoHeight, new_height);
        canvas_track.applyConstraints({width: {min: canvas_width + 1}})
          .then(() => {
            failTest("applyConstraints() should not succeed at setting the " +
                     "track size to a value greater than the source size.");
          })
          .catch(e => {
            assertEquals(e.name, 'OverconstrainedError');
            assertEquals(e.constraint, 'width');
            reportTestSuccess();
          });
      }
      canvas_track.applyConstraints({width: new_width, height: new_height})
        .then(() => {
          ctx.lineTo(100,100);
          ctx.stroke();
        })
        .catch(e => failTest("Unexpected error: " + e));
    }
    video.srcObject = canvas_stream;
  }

  function concurrentGetUserMediaStop() {
    var num_stopped = 0;
    const N = 250;
    for (var i = 0; i < N; i++) {
      navigator.mediaDevices.getUserMedia({video: true}).then(s => {
        setTimeout(() => {
          s.getVideoTracks()[0].stop();
          if (++num_stopped == N)
            reportTestSuccess();
        }, 1);
      }).catch(failTest);
    }
  }

  function getUserMediaAfterStopCanvasCapture() {
    var canvas = document.createElement('canvas');
    canvas.width = canvas.height = 64;

    var stream = canvas.captureStream();
    assertTrue(stream, 'Error creating MediaStream');
    assertEquals(1, stream.getVideoTracks().length);
    assertEquals(0, stream.getAudioTracks().length);

    stream.getVideoTracks()[0].stop();
    navigator.mediaDevices.getUserMedia({video: true}).then(s => {
      s.getVideoTracks()[0].stop();
      reportTestSuccess();
    }).catch(failTest);
  }

  async function getUserMediaEchoCancellationOnAndOff() {
    try {
      var streams;

      streams = await navigator.mediaDevices.getUserMedia({
          audio: {
              deviceId: {exact: "default"},
              echoCancellation: {exact: true}
          }
      });
      assertTrue(streams.getAudioTracks()[0].getSettings().echoCancellation);

      streams = await navigator.mediaDevices.getUserMedia({
          audio: {
              deviceId: {exact: "default"},
              echoCancellation: {exact: false}
          }
      });
      assertFalse(streams.getAudioTracks()[0].getSettings().echoCancellation);
      reportTestSuccess();
    } catch(err) {
      failTest('getUserMedia() failed, error: ' + err.message);
    }
  }

  async function getUserMediaEchoCancellationOnAndOffAndVideo() {
    try {
      var streams;

      streams = await navigator.mediaDevices.getUserMedia({
          video: true,
          audio: {
              deviceId: {exact: "default"},
              echoCancellation: {exact: true}
          }
      });
      assertTrue(streams.getAudioTracks()[0].getSettings().echoCancellation);

      streams = await navigator.mediaDevices.getUserMedia({
          video: true,
          audio: {
              deviceId: {exact: "default"},
              echoCancellation: {exact: false}
          }
      });
      assertFalse(streams.getAudioTracks()[0].getSettings().echoCancellation);
      reportTestSuccess();
    } catch(err) {
      failTest('getUserMedia() failed, error: ' + err.message);
    }
  }

  var resolveTrackEndedAfterServiceCrash;
  var trackEndedAfterServiceCrash = new Promise(function(resolve, reject) {
    resolveTrackEndedAfterServiceCrash = resolve;
  });

  async function setUpForAudioServiceCrash() {
    try {
      // Subscribe to device notifications to force immediate reconnection
      // to the audio service and verify that the browser does not crash.
      navigator.mediaDevices.ondevicechange = () => {};
      let stream = await navigator.mediaDevices.getUserMedia({audio: true});
      let track = stream.getAudioTracks()[0];
      assertTrue(stream.active);
      assertEquals(track.readyState, "live");

      // This expects the audio track to be ended due to a crash in the audio
      // service.
      track.onended = () => {
        resolveTrackEndedAfterServiceCrash(track);
      }

      sendValueToTest("OK");
    } catch (err) {
      failTest('getUserMedia() failed');
    }
  }

  async function verifyAfterAudioServiceCrash() {
    let track = await trackEndedAfterServiceCrash;
    assertEquals(track.readyState, "ended");

    // getUserMedia() should work after the service restarts.
    try {
      let stream = await navigator.mediaDevices.getUserMedia({audio: true});
      assertTrue(stream.active);
      assertEquals(stream.getAudioTracks()[0].readyState, "live");
      reportTestSuccess();
    } catch(err) {
      failTest('getUserMedia() failed');
    }
  }

  function playStreamAndGetElement(stream) {
    v = document.createElement("video");
    document.body.append(v);
    v.srcObject = stream;
    v.play();
    return v;
  }

  async function getUserMediaCloneAndApplyConstraints() {
    const stream = await navigator.mediaDevices.getUserMedia({video: { width: 640, height: 480 }});
    const video = playStreamAndGetElement(stream);

    const clone1 = stream.clone();
    await clone1.getVideoTracks()[0].applyConstraints({width: 500, height: 500});
    const video1 = playStreamAndGetElement(clone1);

    const clone2 = stream.clone();
    await clone2.getVideoTracks()[0].applyConstraints({width: 100, height: 100});
    const video2 = playStreamAndGetElement(clone2);

    var numFrames = 0;
    var numOnProgress = 0;
    video2.onprogress = () => {
      numFrames += video2.webkitDecodedFrameCount;
      ++numOnProgress;
      if (numFrames >= 1)
        reportTestSuccess();
      if (numOnProgress > 20)
        failTest("track from cloned stream is not producing any frames");
    }
  }

  async function enumerationAfterSameDocumentNaviagtion() {
    let stream = await navigator.mediaDevices.getUserMedia({ audio:true, video:true });
    stream.getTracks().forEach(t => t.stop());
    let devices1 = await navigator.mediaDevices.enumerateDevices();

    // Navigate within the same document and enumerate again.
    window.location = "#navigate";
    let devices2 = await navigator.mediaDevices.enumerateDevices();
    assertEquals(devices1.length, devices2.length);
    for (let i=0; i<devices1.length; i++) {
      assertEquals(devices1[i].deviceId, devices2[i].deviceId);
    }
    reportTestSuccess();
  }

  </script>
</head>
<body>
  <table border="0">
    <!-- Canvases are named after their corresponding video elements. -->
    <tr>
      <td><video id="local-view-1" width="320" height="240" autoplay
          style="display:none"></video></td>
      <td><canvas id="local-view-1-canvas" width="320" height="240"
          style="display:none"></canvas></td>
    </tr>
    <tr>
      <td><video id="local-view-2" width="320" height="240" autoplay
          style="display:none"></video></td>
      <td><canvas id="local-view-2-canvas" width="320" height="240"
          style="display:none"></canvas></td>
    </tr>
  </table>
  <p>Some text so the document is ready for input.</p>
</body>
</html>
